use core::fmt;
use serde::Deserialize;
use std::{error, fs, io, process};

fn main() {
    // input path
    println!("Input path:");
    let mut input_path = String::new();
    io::stdin()
        .read_line(&mut input_path)
        .expect("Failed to read input path");
    let input_path = input_path.trim();
    println!("Input output path:");
    let mut output_path = String::new();
    io::stdin()
        .read_line(&mut output_path)
        .expect("Failed to read output path");
    let output_path = output_path.trim();

    match into_workspace(input_path, output_path) {
        Ok(_) => (),
        Err(e) => {
            eprintln!("Error: {}", e);
            process::exit(1);
        }
    }
}

#[derive(Debug)]
enum BiliError {
    NotFound(String),
    PermissionDenied(String),
    FFMpegError(String),
    InfoError(String),
    Other(String),
}
impl fmt::Display for BiliError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::NotFound(s) => write!(f, "File not found: {}", s),
            Self::PermissionDenied(s) => write!(f, "Permission denied: {}", s),
            Self::FFMpegError(s) => write!(f, "FFMpeg error: {}", s),
            Self::InfoError(s) => write!(f, "Info error: {}", s),
            Self::Other(s) => write!(f, "Error: {}", s),
        }
    }
}
impl error::Error for BiliError {}

fn into_workspace(input_path: &str, output_path: &str) -> Result<(), BiliError> {
    for avid in fs::read_dir(input_path).or_else(|e| match e.kind() {
        io::ErrorKind::NotFound => Err(BiliError::NotFound(format!("{}", input_path))),
        io::ErrorKind::PermissionDenied => {
            Err(BiliError::PermissionDenied(format!("{}", input_path)))
        }
        _ => Err(BiliError::Other(format!("{}: {}", input_path, e))),
    })? {
        if let Ok(avid) = avid {
            let avid_path = avid.path();
            if !avid_path.is_dir() {
                continue;
            }
            for cid in fs::read_dir(avid_path).unwrap() {
                let cid_path = cid.unwrap().path();
                let cid_path = cid_path.to_str().unwrap();
                let entry = read_entry(&format!("{}/entry.json", cid_path))?;

                let video_path = format!("{}/{}/video.m4s", cid_path, entry.type_tag);
                let audio_path = format!("{}/{}/audio.m4s", cid_path, entry.type_tag);
                let folder_path = format!("{}/{}", output_path, name_check(&entry.title));
                match fs::create_dir_all(&folder_path) {
                    Ok(_) => Ok(()),
                    Err(e) => match e.kind() {
                        io::ErrorKind::NotFound => {
                            Err(BiliError::NotFound(format!("{}", folder_path)))
                        }
                        io::ErrorKind::PermissionDenied => {
                            Err(BiliError::PermissionDenied(format!("{}", folder_path)))
                        }
                        _ => Err(BiliError::Other(format!("{}: {}", folder_path, e))),
                    },
                }?;
                let file_name = format!(
                    "{}.mp4",
                    if let Some(part) = entry.page_data.part {
                        part
                    } else {
                        entry.title
                    }
                );
                let args = [
                    "-i",
                    &video_path,
                    "-i",
                    &audio_path,
                    "-c:v",
                    "copy",
                    "-c:a",
                    "copy",
                    &format!("{}/{}", &folder_path, name_check(&file_name)),
                ];
                match ffmpeg_process(&args) {
                    Ok(_) => (),
                    Err(e) => {
                        return Err(BiliError::FFMpegError(format!("{}: {}", file_name, e)));
                    }
                }
            }
        }
    }

    Ok(())
}

#[allow(unused)]
#[derive(Deserialize, Debug)]
struct Entry {
    pub title: String,
    pub type_tag: String,
    avid: u128,
    bvid: String,
    pub page_data: PageData,
}

#[allow(unused)]
#[derive(Deserialize, Debug)]
struct PageData {
    cid: u128,
    pub part: Option<String>,
}

fn read_entry(file: &str) -> Result<Entry, BiliError> {
    match fs::read_to_string(file) {
        Ok(info) => match serde_json::from_str(&info) {
            Ok(entry) => Ok(entry),
            Err(_) => Err(BiliError::InfoError(format!(
                "entry cannot be parsed: {}",
                file
            ))),
        },
        Err(e) => match e.kind() {
            io::ErrorKind::NotFound => Err(BiliError::NotFound(format!("{}", file))),
            io::ErrorKind::PermissionDenied => {
                Err(BiliError::PermissionDenied(format!("{}", file)))
            }
            _ => Err(BiliError::Other(format!("{}: {}", file, e))),
        },
    }
}

fn name_check(name: &str) -> String {
    let invalid_chars = ['<', '>', ':', '"', '/', '\\', '|', '?', '*', '\0'];

    let filtered_name: String = name
        .chars()
        .filter(|&c| !invalid_chars.contains(&c))
        .collect();

    filtered_name
}

fn ffmpeg_process(args: &[&str]) -> io::Result<process::Output> {
    process::Command::new("ffmpeg").args(args).output()
}
